package com.base.interceptor;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.base.annotation.Unsecured;
import com.base.context.Context;
import com.base.context.ContextHolder;
import com.base.context.ContextImpl;
import com.base.helper.IDGenerator;
import com.base.helper.Result;
import com.base.helper.StringUtils;
import com.message.api.user.vo.AcUserVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ClassUtils;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static com.base.context.ContextHolder.appendContext;

/**
 * @author mzl
 * @desc
 */
public class ContextInjectInterceptor extends HandlerInterceptorAdapter {

    public static final  String                   SWAGGER_TOKEN = "fbc545a91cc94fe89296828a25a7e08e" +
            "@9085553879028596738";
    public static final  String                   TOKEN         = "token";
    public static final  String                   URI           = "uri";
    public static final  String                   ERROR_CODE    = "errorCode";
    public static final  String                       AUTHORIZED    = "authorized";
    public static final  String                   SWAGGER       = "swagger";
    private static final Logger                   LOG           = LoggerFactory.getLogger("ContextInjectInterceptor");
    @Autowired
    private              Environment              env;
    private              ScheduledExecutorService executorService;

    /**
     * 判断token是否为空
     *
     * @param token
     * @return
     */
    private static boolean isEmptyToken(String token) {
        return StringUtils.isEmpty(token) || StringUtils.isEquals(token, "null");
    }

    /**
     * 在cookies中查找token
     *
     * @param request
     * @return
     */
    private static String fromCookies(HttpServletRequest request) {
        String token = null;
        // 未登录则， 从cookie中尝试获取token
        final Cookie[] cookies = request.getCookies();
        if (null != cookies) {
            for (Cookie cookie : cookies) {
                final String name = cookie.getName();
                if (StringUtils.isEquals(TOKEN, name)) {
                    token = cookie.getValue();
                    break;
                }
            }
        }
        return token;
    }

    /**
     * 在cookies中查找uri
     *
     * @param request
     * @return
     */
    private static String pageUriFromCookies(HttpServletRequest request) {
        String uri = null;
        // 未登录则， 从cookie中尝试获取token
        final Cookie[] cookies = request.getCookies();
        if (null != cookies) {
            for (Cookie cookie : cookies) {
                final String name = cookie.getName();
                if (StringUtils.isEquals(URI, name)) {
                    uri = cookie.getValue();
                    break;
                }
            }
        }
        return uri;
    }

    /**
     * 未登录时默认的用户身份
     *
     * @param token
     * @return
     */
    private static Context buildAnoymouseContext(String token) {
        Context  context;//
        AcUserVo user = new AcUserVo();
        user.setUsername("ANONYMOUS");
        context = new ContextImpl(token, user);
        appendContext(context);
        if (LOG.isDebugEnabled()) {
            LOG.debug("当前用户未登录");
        }
        return context;
    }

    @PostConstruct
    public void init() {
        // 启动线程
        executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                // 清除过期context
                try {
                    long                 now      = System.currentTimeMillis();
                    long                 diff     = Duration.ofMinutes(30).toMillis();
                    Map<String, Context> contexts = ContextHolder.contexts();
                    contexts.forEach((token, context) -> {
                        long time = context.getLastAccessTime().getTime();
                        if (Long.compare(now - time, diff) >= 0) {
                            ContextHolder.remove(token);
                        }
                    });
                } catch (Exception e) {
                    //
                }
            }
        }, 5L, 3L, TimeUnit.SECONDS);
    }

    @PreDestroy
    public void shutdown() {
        if (null != executorService) {
            try {
                executorService.shutdownNow();
            } catch (Exception e) {
                //
            }
        }
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, //
                             final HttpServletResponse response, //
                             final Object handler) throws Exception {
        //
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "PUT, POST, GET, DELETE,OPTIONS");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
                "X-Requested-With, X-Access-Token, X-Upload-Auth-Token, Origin, Content-Type, Cookie, " + TOKEN + "," + URI);
        response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, TOKEN + ", " +URI + ", " + ERROR_CODE + ", " + AUTHORIZED);
        //
        if (CorsUtils.isPreFlightRequest(request)) {
            return true;
        } else {
            if (handler instanceof HandlerMethod) {
                HandlerMethod hm          = (HandlerMethod) handler;
                Class<?>      targetClass = AopUtils.getTargetClass(hm.getBean());
                if (targetClass.getPackage().getName().contains(SWAGGER)) {
                    return super.preHandle(request, response, handler);
                }
                if (BasicErrorController.class.isAssignableFrom(targetClass)) {
                    return super.preHandle(request, response, handler);
                }
                Method    method    = hm.getMethod();
                Unsecured unsecured = findAnnotation(targetClass, method, Unsecured.class);
                //
                Context context = retrieveContext(request);
                ((ContextImpl) context).setLastAccessTime(new Date());
                ContextHolder.bindContext(context);
                //当前访问的页面
                String pageUri =retrievePageUri(request);
                //在请求头和响应头中设置一些自定义的字段，token、URI、errorCode和authorized。
                response.setHeader(TOKEN, context.currentToken());
                response.setHeader(URI,pageUri);
                appendCookie4Token(context, request, response);
                response.setHeader(AUTHORIZED, String.valueOf(context.isAuthorized()));

                if (null == unsecured) {
                    //登录验证
                    if (!context.isAuthorized() && !CorsUtils.isPreFlightRequest(request)) {
                        mustLogined(request, response);
                        return false;
                    }
                    //页面验证
                    if(!matchUri(context,pageUri)&&!StringUtils.isEquals(context.currentToken(),
                            ContextInjectInterceptor.SWAGGER_TOKEN)){
                        noAccessTo(request,response);
                        return false;
                    }
                }
            }
            return true;
        }
    }

    /**
     * 只有登录成功后才可访问
     */
    private void mustLogined(HttpServletRequest request, HttpServletResponse response) {
        Context context = ContextHolder.currentContext();
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader(ERROR_CODE, "401");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, TOKEN + ",errorCode");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
                "X-Requested-With,X-Access-Token,X-Upload-Auth-Token,Origin,Content-Type,Cookie,user_type," +
                        TOKEN);
        if (!CorsUtils.isPreFlightRequest(request)) {
            response.setHeader(AUTHORIZED, String.valueOf(context.isAuthorized()));
        }
        if (LOG.isErrorEnabled()) {
            LOG.error("只有登录成功后才可访问{}", request.getRequestURI());
        }
        try (PrintWriter out = response.getWriter()) {
            out.write(JSON.toJSONString(new Result(false, "请登录", "只有登录成功后才可访问"), SerializerFeature.QuoteFieldNames,
                    SerializerFeature.BrowserCompatible, SerializerFeature.WriteEnumUsingToString));
        } catch (Exception e) {
            //
        }
    }

    /**
     * 无权限访问
     */
    private void noAccessTo(HttpServletRequest request, HttpServletResponse response) {
        Context context = ContextHolder.currentContext();
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader(ERROR_CODE, "403");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, TOKEN + ",errorCode");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
                "X-Requested-With,X-Access-Token,X-Upload-Auth-Token,Origin,Content-Type,Cookie,user_type," +
                        TOKEN);
        if (!CorsUtils.isPreFlightRequest(request)) {
            response.setHeader(AUTHORIZED, String.valueOf(context.isAuthorized()));
        }
        if (LOG.isErrorEnabled()) {
            LOG.error("当前角色无权限访问路径{}", request.getRequestURI());
        }
        try (PrintWriter out = response.getWriter()) {
            out.write(JSON.toJSONString(new Result(false, "访问受限", "当前登录角色无权限访问该接口"), SerializerFeature.QuoteFieldNames,
                    SerializerFeature.BrowserCompatible, SerializerFeature.WriteEnumUsingToString));
        } catch (Exception e) {
            //
        }
    }

    /**
     * 根据token获取上下文信息
     *
     * @param request
     * @return
     */
    private Context retrieveContext(HttpServletRequest request) {
        Context context;
        String  token = request.getHeader(TOKEN);
        LOG.info("头部得到token: " + token);
        if (isEmptyToken(token)) {
            token = fromCookies(request);
            LOG.info("cookie得到token: " + token);
        }
        if (isEmptyToken(token)) {
            token   = UUID.randomUUID().toString().replaceAll("-", "") + "@" + IDGenerator.generateIDStr();
            context = buildAnoymouseContext(token);
        } else {
            try {
                token = URLDecoder.decode(token, "utf-8");
            }catch (UnsupportedEncodingException e){
                LOG.error("token转码失败，token={}", token);
            }
            context = ContextHolder.contextOf(token);
            if (null == context) {
                if (null == context) {
                    context = buildAnoymouseContext(token);
                }
            }
        }
        return context;
    }

    /**
     * 将token写进cookie
     *
     * @param context
     * @param request
     * @param response
     */
    private void appendCookie4Token(Context context, HttpServletRequest request, HttpServletResponse response) {
        String token     = context.currentToken();
        Cookie newCookie = null;
        // 未登录则， 从cookie中尝试获取token
        final Cookie[] cookies = request.getCookies();
        if (null != cookies) {
            for (Cookie cookie : cookies) {
                final String name = cookie.getName();
                if (StringUtils.isEquals(TOKEN, name)) {
                    newCookie = cookie;
                }
            }
        }
        if (null == newCookie) {
            newCookie = new Cookie(TOKEN, token);
        } else {
            newCookie.setValue(token);
        }

        newCookie.setPath("/");
        newCookie.setMaxAge((int) Duration.ofHours(12).getSeconds());
        // 写回cookie
        response.addCookie(newCookie);
        //
        if (LOG.isDebugEnabled()) {
            LOG.debug("token[{}]写回cookie", token);
        }
    }

    /**
     * 获取当前访问的页面
     * @param request
     * @return
     */
    private String retrievePageUri(HttpServletRequest request){
        String  uri = request.getHeader(URI);
        LOG.info("头部得到uri: " + uri);
        if (isEmptyToken(uri)) {
            uri = pageUriFromCookies(request);
            LOG.info("cookie得到uri: " + uri);
        }
        if (!isEmptyToken(uri)) {
            try {
                uri = URLDecoder.decode(uri, "utf-8");
            }catch (UnsupportedEncodingException e){
                LOG.error("token转码失败，uri={}", uri);
            }
        }
        return uri;
    }

    /**
     * 匹配页面
     * @param context
     * @param uri
     * @return
     */
    private Boolean matchUri(Context context,String uri){
        List<String> accessUris = context.getAccessUris();
        if(null!=accessUris&&accessUris.size()>0){
            boolean flag = accessUris.contains(uri);
            return flag;
        }
        return false;
    }

    /**
     * 获取方法注解
     *
     * @param targetClass
     * @param method
     * @param annotationType
     * @param <A>
     * @return
     */
    private <A extends Annotation> A findAnnotation(final Class<?> targetClass, //
                                                    final Method method, //
                                                    final Class<A> annotationType) {
        Class<?> userClass      = ClassUtils.getUserClass(targetClass);
        Method   specificMethod = ClassUtils.getMostSpecificMethod(method, userClass);
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
        A annotation = AnnotationUtils.findAnnotation(specificMethod, annotationType);
        if ((null == annotation) && (specificMethod != method)) {
            annotation = AnnotationUtils.findAnnotation(method, annotationType);
        }
        return annotation;
    }

    /**
     * 去除当前线程上下文信息
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(final HttpServletRequest request, //
                                final HttpServletResponse response, //
                                final Object handler, final Exception ex) throws Exception {
        //
        ContextHolder.unbindContext();
        //
        super.afterCompletion(request, response, handler, ex);
    }

}
